﻿function ExtInfoWindow(marker, windowId, html, opt_opts,map) {
    this.html_ = html;
    this.marker_ = marker;
    this.infoWindowId_ = windowId;

    this.options_ = opt_opts == null ? {} : opt_opts;
    this.ajaxUrl_ = this.options_.ajaxUrl == null ? null : this.options_.ajaxUrl;
    this.callback_ = this.options_.ajaxCallback == null ? null : this.options_.ajaxCallback;

    this.borderSize_ = this.options_.beakOffset == null ? 0 : this.options_.beakOffset;
    this.paddingX_ = this.options_.paddingX == null ? 0 + this.borderSize_ : this.options_.paddingX + this.borderSize_;
    this.paddingY_ = this.options_.paddingY == null ? 0 + this.borderSize_ : this.options_.paddingY + this.borderSize_;

    this.map_ = map;
    this.setMap(map);

    this.container_ = document.createElement('div');
    this.container_.style.position = 'relative';
    this.container_.style.display = 'none';

    this.contentDiv_ = document.createElement('div');
    this.contentDiv_.id = this.infoWindowId_ + '_contents';
    this.contentDiv_.innerHTML = this.html_;
    this.contentDiv_.style.display = 'block';
    this.contentDiv_.style.visibility = 'hidden';

    this.wrapperDiv_ = document.createElement('div');
};

//use the GOverlay class
ExtInfoWindow.prototype = new google.maps.OverlayView();

/**
* Called by GMap2's addOverlay method.  Creates the wrapping div for our info window and adds
* it to the relevant map pane.  Also binds mousedown event to a private function so that they
* are not passed to the underlying map.  Finally, performs ajax request if set up to use ajax
* in the constructor.
* @param {GMap2} map The map that has had this extInfoWindow is added to it.
*/
ExtInfoWindow.prototype.onAdd = function () {
    this.map_ = map;

    this.defaultStyles = {
        containerWidth: this.map_.getSize().width / 2,
        borderSize: 1
    };

    this.wrapperParts = {
        tl: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        t: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        tr: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        l: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        r: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        bl: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        b: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        br: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        beak: { t: 0, l: 0, w: 0, h: 0, domElement: null },
        close: { t: 0, l: 0, w: 0, h: 0, domElement: null }
    };

    for (var i in this.wrapperParts) {
        var tempElement = document.createElement('div');
        tempElement.id = this.infoWindowId_ + '_' + i;
        tempElement.style.visibility = 'hidden';
        document.body.appendChild(tempElement);
        tempElement = document.getElementById(this.infoWindowId_ + '_' + i);
        var tempWrapperPart = eval('this.wrapperParts.' + i);
        tempWrapperPart.w = parseInt(this.getStyle_(tempElement, 'width'));
        tempWrapperPart.h = parseInt(this.getStyle_(tempElement, 'height'));
        document.body.removeChild(tempElement);
    }
    for (var i in this.wrapperParts) {
        if (i == 'close') {
            //first append the content so the close button is layered above it
            this.wrapperDiv_.appendChild(this.contentDiv_);
        }
        var wrapperPartsDiv = null;
        if (this.wrapperParts[i].domElement == null) {
            wrapperPartsDiv = document.createElement('div');
            this.wrapperDiv_.appendChild(wrapperPartsDiv);
        } else {
            wrapperPartsDiv = this.wrapperParts[i].domElement;
        }
        wrapperPartsDiv.id = this.infoWindowId_ + '_' + i;
        wrapperPartsDiv.style.position = 'absolute';
        wrapperPartsDiv.style.width = this.wrapperParts[i].w + 'px';
        wrapperPartsDiv.style.height = this.wrapperParts[i].h + 'px';
        wrapperPartsDiv.style.top = this.wrapperParts[i].t + 'px';
        wrapperPartsDiv.style.left = this.wrapperParts[i].l + 'px';
        this.wrapperParts[i].domElement = wrapperPartsDiv;
    }

    this.getPanes.floatPane.appendChild(this.container_);
    this.container_.id = this.infoWindowId_;
    var containerWidth = this.getStyle_(document.getElementById(this.infoWindowId_), 'width');
    this.container_.style.width = (containerWidth == null ? this.defaultStyles.containerWidth : containerWidth);

    this.map_.getDiv().appendChild(this.contentDiv_);
    this.contentWidth = this.getDimensions_(this.container_).width;
    this.contentDiv_.style.width = this.contentWidth + 'px';
    this.contentDiv_.style.position = 'absolute';

    this.container_.appendChild(this.wrapperDiv_);

    google.maps.event.addDomListener(this.container_, 'mousedown', this, this.onClick_);
    google.maps.event.addDomListener(this.container_, 'dblclick', this, this.onClick_);
    //google.maps.event.addDomListener(this.container_, 'DOMMouseScroll', this, this.onClick_);


    google.maps.event.trigger(this.map_, 'extinfowindowopen');
    if (this.ajaxUrl_ != null) {
        this.ajaxRequest_(this.ajaxUrl_);
    }
};

/**
* Private function to steal mouse click events to prevent it from returning to the map.
* Without this links in the ExtInfoWindow would not work, and you could click to zoom or drag 
* the map behind it.
* @private
* @param {MouseEvent} e The mouse event caught by this function
*/
ExtInfoWindow.prototype.onClick_ = function (e) {
    if (navigator.userAgent.toLowerCase().indexOf('msie') != -1 && document.all) {
        window.event.cancelBubble = true;
        window.event.returnValue = false;
    } else {
        //e.preventDefault();
        e.stopPropagation();
    }
};

/**
* Remove the extInfoWindow container from the map pane. 
*/
ExtInfoWindow.prototype.remove = function () {
    if (this.map_.getExtInfoWindow() != null) {
        google.maps.event.trigger(this.map_, 'extinfowindowbeforeclose');

        google.maps.event.clearInstanceListeners(this.container_);
        if (this.container_.outerHTML) {
            this.container_.outerHTML = ''; //prevent pseudo-leak in IE
        }
        if (this.container_.parentNode) {
            this.container_.parentNode.removeChild(this.container_);
        }
        this.container_ = null;
        google.maps.event.trigger(this.map_, 'extinfowindowclose');
        this.map_.setExtInfoWindow_(null);
    }
};

/**
* Return a copy of this overlay, for the parent Map to duplicate itself in full. This
* is part of the Overlay interface and is used, for example, to copy everything in the 
* main view into the mini-map.
* @return {GOverlay}
*/
ExtInfoWindow.prototype.copy = function () {
    return new ExtInfoWindow(this.marker_, this.infoWindowId_, this.html_, this.options_);
};

/**
* Draw extInfoWindow and wrapping decorators onto the map.  Resize and reposition
* the map as necessary. 
* @param {Boolean} force Will be true when pixel coordinates need to be recomputed.
*/
ExtInfoWindow.prototype.redraw = function (force) {
    if (!force || this.container_ == null) return;

    //set the content section's height, needed so  browser font resizing does not affect the window's dimensions
    var contentHeight = this.contentDiv_.offsetHeight;
    this.contentDiv_.style.height = contentHeight + 'px';

    //reposition contents depending on wrapper parts.
    //this is necessary for content that is pulled in via ajax
    this.contentDiv_.style.left = this.wrapperParts.l.w + 'px';
    this.contentDiv_.style.top = this.wrapperParts.tl.h + 'px';
    this.contentDiv_.style.visibility = 'visible';

    //Finish configuring wrapper parts that were not set in initialization
    this.wrapperParts.tl.t = 0;
    this.wrapperParts.tl.l = 0;
    this.wrapperParts.t.l = this.wrapperParts.tl.w;
    this.wrapperParts.t.w = (this.wrapperParts.l.w + this.contentWidth + this.wrapperParts.r.w) - this.wrapperParts.tl.w - this.wrapperParts.tr.w;
    this.wrapperParts.t.h = this.wrapperParts.tl.h;
    this.wrapperParts.tr.l = this.wrapperParts.t.w + this.wrapperParts.tl.w;
    this.wrapperParts.l.t = this.wrapperParts.tl.h;
    this.wrapperParts.l.h = contentHeight;
    this.wrapperParts.r.l = this.contentWidth + this.wrapperParts.l.w;
    this.wrapperParts.r.t = this.wrapperParts.tr.h;
    this.wrapperParts.r.h = contentHeight;
    this.wrapperParts.bl.t = contentHeight + this.wrapperParts.tl.h;
    this.wrapperParts.b.l = this.wrapperParts.bl.w;
    this.wrapperParts.b.t = contentHeight + this.wrapperParts.tl.h;
    this.wrapperParts.b.w = (this.wrapperParts.l.w + this.contentWidth + this.wrapperParts.r.w) - this.wrapperParts.bl.w - this.wrapperParts.br.w;
    this.wrapperParts.b.h = this.wrapperParts.bl.h;
    this.wrapperParts.br.l = this.wrapperParts.b.w + this.wrapperParts.bl.w;
    this.wrapperParts.br.t = contentHeight + this.wrapperParts.tr.h;
    this.wrapperParts.close.l = this.wrapperParts.tr.l + this.wrapperParts.tr.w - this.wrapperParts.close.w - this.borderSize_;
    this.wrapperParts.close.t = this.borderSize_;
    this.wrapperParts.beak.l = this.borderSize_ + (this.contentWidth / 2) - (this.wrapperParts.beak.w / 2);
    this.wrapperParts.beak.t = this.wrapperParts.bl.t + this.wrapperParts.bl.h - this.borderSize_;

    //create the decoration wrapper DOM objects
    //append the styled info window to the container
    for (var i in this.wrapperParts) {
        if (i == 'close') {
            //first append the content so the close button is layered above it
            this.wrapperDiv_.insertBefore(this.contentDiv_, this.wrapperParts[i].domElement);
        }
        var wrapperPartsDiv = null;
        if (this.wrapperParts[i].domElement == null) {
            wrapperPartsDiv = document.createElement('div');
            this.wrapperDiv_.appendChild(wrapperPartsDiv);
        } else {
            wrapperPartsDiv = this.wrapperParts[i].domElement;
        }
        wrapperPartsDiv.id = this.infoWindowId_ + '_' + i;
        wrapperPartsDiv.style.position = 'absolute';
        wrapperPartsDiv.style.width = this.wrapperParts[i].w + 'px';
        wrapperPartsDiv.style.height = this.wrapperParts[i].h + 'px';
        wrapperPartsDiv.style.top = this.wrapperParts[i].t + 'px';
        wrapperPartsDiv.style.left = this.wrapperParts[i].l + 'px';
        this.wrapperParts[i].domElement = wrapperPartsDiv;
    }

    //add event handler for the close box
    var currentMarker = this.marker_;
    var thisMap = this.map_;
    google.maps.event.addDomListener(this.wrapperParts.close.domElement, 'click',
    function () {
        thisMap.closeExtInfoWindow();
    }
  );

    //position the container on the map, over the marker
    var pixelLocation = this.map_.fromLatLngToDivPixel(this.marker_.getPoint());
    this.container_.style.position = 'absolute';
    var markerIcon = this.marker_.getIcon();
    this.container_.style.left = (pixelLocation.x
    - (this.contentWidth / 2)
    - markerIcon.iconAnchor.x
    + markerIcon.infoWindowAnchor.x
  ) + 'px';

    this.container_.style.top = (pixelLocation.y
    - this.wrapperParts.bl.h
    - contentHeight
    - this.wrapperParts.tl.h
    - this.wrapperParts.beak.h
    - markerIcon.iconAnchor.y
    + markerIcon.infoWindowAnchor.y
    + this.borderSize_
  ) + 'px';

    this.container_.style.display = 'block';

    if (this.map_.getExtInfoWindow() != null) {
        this.repositionMap_();
    }
};

/**
* Determine the dimensions of the contents to recalculate and reposition the 
* wrapping decorator elements accordingly.
*/
ExtInfoWindow.prototype.resize = function () {

    //Create temporary DOM node for new contents to get new height
    //This is done because if you manipulate this.contentDiv_ directly it causes visual errors in IE6
    var tempElement = this.contentDiv_.cloneNode(true);
    tempElement.id = this.infoWindowId_ + '_tempContents';
    tempElement.style.visibility = 'hidden';
    tempElement.style.height = 'auto';
    document.body.appendChild(tempElement);
    tempElement = document.getElementById(this.infoWindowId_ + '_tempContents');
    var contentHeight = tempElement.offsetHeight;
    document.body.removeChild(tempElement);

    //Set the new height to eliminate visual defects that can be caused by font resizing in browser
    this.contentDiv_.style.height = contentHeight + 'px';

    var contentWidth = this.contentDiv_.offsetWidth;
    var pixelLocation = this.map_.fromLatLngToDivPixel(this.marker_.getPoint());

    var oldWindowHeight = this.wrapperParts.t.domElement.offsetHeight + this.wrapperParts.l.domElement.offsetHeight + this.wrapperParts.b.domElement.offsetHeight;
    var oldWindowPosTop = this.wrapperParts.t.domElement.offsetTop;

    //resize info window to look correct for new height
    this.wrapperParts.l.domElement.style.height = contentHeight + 'px';
    this.wrapperParts.r.domElement.style.height = contentHeight + 'px';
    var newPosTop = this.wrapperParts.b.domElement.offsetTop - contentHeight;
    this.wrapperParts.l.domElement.style.top = newPosTop + 'px';
    this.wrapperParts.r.domElement.style.top = newPosTop + 'px';
    this.contentDiv_.style.top = newPosTop + 'px';
    windowTHeight = parseInt(this.wrapperParts.t.domElement.style.height);
    newPosTop -= windowTHeight;
    this.wrapperParts.close.domElement.style.top = newPosTop + this.borderSize_ + 'px';
    this.wrapperParts.tl.domElement.style.top = newPosTop + 'px';
    this.wrapperParts.t.domElement.style.top = newPosTop + 'px';
    this.wrapperParts.tr.domElement.style.top = newPosTop + 'px';

    this.repositionMap_();
};

/**
* Check to see if the displayed extInfoWindow is positioned off the viewable 
* map region and by how much.  Use that information to pan the map so that 
* the extInfoWindow is completely displayed.
* @private
*/
ExtInfoWindow.prototype.repositionMap_ = function () {
    //pan if necessary so it shows on the screen
    var mapNE = this.map_.fromLatLngToDivPixel(
    this.map_.getBounds().getNorthEast()
  );
    var mapSW = this.map_.fromLatLngToDivPixel(
    this.map_.getBounds().getSouthWest()
  );
    var markerPosition = this.map_.fromLatLngToDivPixel(
    this.marker_.getPoint()
  );

    var panX = 0;
    var panY = 0;
    var paddingX = this.paddingX_;
    var paddingY = this.paddingY_;
    var infoWindowAnchor = this.marker_.getIcon().infoWindowAnchor;
    var iconAnchor = this.marker_.getIcon().iconAnchor;

    //test top of screen	
    var windowT = this.wrapperParts.t.domElement;
    var windowL = this.wrapperParts.l.domElement;
    var windowB = this.wrapperParts.b.domElement;
    var windowR = this.wrapperParts.r.domElement;
    var windowBeak = this.wrapperParts.beak.domElement;

    var offsetTop = markerPosition.y - (-infoWindowAnchor.y + iconAnchor.y + this.getDimensions_(windowBeak).height + this.getDimensions_(windowB).height + this.getDimensions_(windowL).height + this.getDimensions_(windowT).height + this.paddingY_);
    if (offsetTop < mapNE.y) {
        panY = mapNE.y - offsetTop;
    } else {
        //test bottom of screen
        var offsetBottom = markerPosition.y + this.paddingY_;
        if (offsetBottom >= mapSW.y) {
            panY = -(offsetBottom - mapSW.y);
        }
    }

    //test right of screen
    var offsetRight = Math.round(markerPosition.x + this.getDimensions_(this.container_).width / 2 + this.getDimensions_(windowR).width + this.paddingX_ + infoWindowAnchor.x - iconAnchor.x);
    if (offsetRight > mapNE.x) {
        panX = -(offsetRight - mapNE.x);
    } else {
        //test left of screen
        var offsetLeft = -(Math.round((this.getDimensions_(this.container_).width / 2 - this.marker_.getIcon().iconSize.width / 2) + this.getDimensions_(windowL).width + this.borderSize_ + this.paddingX_) - markerPosition.x - infoWindowAnchor.x + iconAnchor.x);
        if (offsetLeft < mapSW.x) {
            panX = mapSW.x - offsetLeft;
        }
    }

    if (panX != 0 || panY != 0 && this.map_.getExtInfoWindow() != null) {
        this.map_.panBy(new GSize(panX, panY));
    }
};

/**
* Private function that handles performing an ajax request to the server.  The response
* information is assumed to be HTML and is placed inside this extInfoWindow's contents region.
* Last, check to see if the height has changed, and resize the extInfoWindow accordingly.
* @private
* @param {String} url The Url of where to make the ajax request on the server
*/
ExtInfoWindow.prototype.ajaxRequest_ = function (url) {
    var thisMap = this.map_;
    var thisCallback = this.callback_;
    GDownloadUrl(url, function (response, status) {
        var infoWindow = document.getElementById(thisMap.getExtInfoWindow().infoWindowId_ + '_contents');
        if (response == null || status == -1) {
            infoWindow.innerHTML = '<span class="error">ERROR: The Ajax request failed to get HTML content from "' + url + '"</span>';
        } else {
            infoWindow.innerHTML = response;
        }
        if (thisCallback != null) {
            thisCallback();
        }
        thisMap.getExtInfoWindow().resize();
        google.maps.event.trigger(thisMap, 'extinfowindowupdate');
    });
};

/**
* Private function derived from Prototype.js to get a given element's
* height and width
* @private
* @param {Object} element The DOM element that will have height and 
*                    width will be calculated for it.
* @return {Object} Object with keys: width, height
*/
ExtInfoWindow.prototype.getDimensions_ = function (element) {
    var display = this.getStyle_(element, 'display');
    if (display != 'none' && display != null) { // Safari bug
        return { width: element.offsetWidth, height: element.offsetHeight };
    }

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily
    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return { width: originalWidth, height: originalHeight };
};

/**
* Private function derived from Prototype.js to get a given element's
* value that is associated with the passed style
* @private
* @param {Object} element The DOM element that will be checked.
* @param {String} style The style name that will be have it's value returned.
* @return {Object}
*/
ExtInfoWindow.prototype.getStyle_ = function (element, style) {
    var found = false;
    style = this.camelize_(style);
    var value = element.style[style];
    if (!value) {
        if (document.defaultView && document.defaultView.getComputedStyle) {
            var css = document.defaultView.getComputedStyle(element, null);
            value = css ? css[style] : null;
        } else if (element.currentStyle) {
            value = element.currentStyle[style];
        }
    }
    if ((value == 'auto') && (style == 'width' || style == 'height') && (this.getStyle_(element, 'display') != 'none')) {
        if (style == 'width') {
            value = element.offsetWidth;
        } else {
            value = element.offsetHeight;
        }
    }
    return (value == 'auto') ? null : value;
};

/**
* Private function pulled from Prototype.js that will change a hyphened
* style name into camel case.
* @private
* @param {String} element The string that will be parsed and made into camel case
* @return {String}
*/
ExtInfoWindow.prototype.camelize_ = function (element) {
    var parts = element.split('-'), len = parts.length;
    if (len == 1) return parts[0];
    var camelized = element.charAt(0) == '-'
    ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
    : parts[0];

    for (var i = 1; i < len; i++) {
        camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
    }
    return camelized;
};

google.maps.Map.prototype.ExtInfoWindowInstance_ = null;
google.maps.Map.prototype.ClickListener_ = null;
google.maps.Map.prototype.InfoWindowListener_ = null;

/**
* Creates a new instance of ExtInfoWindow for the GMarker.  Register the newly created 
* instance with the map, ensuring only one window is open at a time. If this is the first
* ExtInfoWindow ever opened, add event listeners to the map to close the ExtInfoWindow on 
* zoom and click, to mimic the default GInfoWindow behavior.
*
* @param {GMap} map The GMap2 object where the ExtInfoWindow will open
* @param {String} cssId The id we will use to reference the info window
* @param {String} html The HTML contents
* @param {Object} opt_opts A contianer for optional arguments:
*    {String} ajaxUrl The Url to hit on the server to request some contents 
*    {Number} paddingX The padding size in pixels that the info window will leave on 
*                    the left and right sides of the map when panning is involved.
*    {Number} paddingX The padding size in pixels that the info window will leave on 
*                    the top and bottom sides of the map when panning is involved.
*    {Number} beakOffset The repositioning offset for when aligning the beak element. 
*                    This is used to make sure the beak lines up correcting if the 
*                    info window styling containers a border.
*/
GMarker.prototype.openExtInfoWindow = function (map, cssId, html, opt_opts) {
    if (map == null) {
        throw 'Error in GMarker.openExtInfoWindow: map cannot be null';
        return false;
    }
    if (cssId == null || cssId == '') {
        throw 'Error in GMarker.openExtInfoWindow: must specify a cssId';
        return false;
    }

    map.closeInfoWindow();
    if (map.getExtInfoWindow() != null) {
        map.closeExtInfoWindow();
    }
    if (map.getExtInfoWindow() == null) {
        map.setExtInfoWindow_(new ExtInfoWindow(
      this,
      cssId,
      html,
      opt_opts
    ));
        if (map.ClickListener_ == null) {
            //listen for map click, close ExtInfoWindow if open
            map.ClickListener_ = google.maps.event.addListener(map, 'click',
      function (event) {
          if (!event && map.getExtInfoWindow() != null) {
              map.closeExtInfoWindow();
          }
      }
      );
        }
        if (map.InfoWindowListener_ == null) {
            //listen for default info window open, close ExtInfoWindow if open
            map.InfoWindowListener_ = google.maps.event.addListener(map, 'infowindowopen',
      function (event) {
          if (map.getExtInfoWindow() != null) {
              map.closeExtInfoWindow();
          }
      }
      );
        }
        map.addOverlay(map.getExtInfoWindow());
    }
};

/**
* Remove the ExtInfoWindow instance
* @param {GMap2} map The map where the GMarker and ExtInfoWindow exist
*/
GMarker.prototype.closeExtInfoWindow = function (map) {
    if (map.getExtInfWindow() != null) {
        map.closeExtInfoWindow();
    }
};

/**
* Get the ExtInfoWindow instance from the map
*/
GMap2.prototype.getExtInfoWindow = function () {
    return this.ExtInfoWindowInstance_;
};
/**
* Set the ExtInfoWindow instance for the map
* @private
*/
GMap2.prototype.setExtInfoWindow_ = function (extInfoWindow) {
    this.ExtInfoWindowInstance_ = extInfoWindow;
}
/**
* Remove the ExtInfoWindow from the map
*/
GMap2.prototype.closeExtInfoWindow = function () {
    if (this.getExtInfoWindow() != null) {
        this.ExtInfoWindowInstance_.remove();
    }
};
